package net;

import gui.BoardGUI;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Random;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import logic.Team;
import messages.ConnectionEstablished;
import messages.GameEventDispatcher;
import messages.GameEventListener;
import messages.GameMessage;
import messages.MovePiece;
import messages.NewGame;
import messages.PlayerDisconnected;
import messages.ResetMatch;
import messages.RotatePiece;
import messages.UmountObelisk;

public class GameServer implements Runnable, GameEventDispatcher{
	
	
	private ArrayList<GameEventListener> listeners;
	
	private boolean run = false;
	private DatagramSocket socket;
    private DocumentBuilderFactory dbf;
    private String currentPlayer;
    private InetAddress currentPlayerAddr;
    private int currentPlayerPort;
    
    private BoardGUI board;
    private Team serverColor;
    private Team playerColor;
    private boolean playing = false;
    private String serverName = "test.server.name";
    private String remotePlayerName;
    
    private KeepAliveDaemon keepalive;
    
	
	public GameServer(BoardGUI board, int listeningPort, Team player) throws SocketException{
		this.serverColor = player;
		if(player==Team.RED)
			playerColor=Team.GRAY;
		else
			playerColor=Team.RED;
		this.board = board;
		dbf = DocumentBuilderFactory.newInstance();
		openConnection(listeningPort);
		Thread thisThread = new Thread(this);
		run = true;
		thisThread.start();
	}
	
	private void openConnection(int port) throws SocketException{
		socket = new DatagramSocket(port);
	}

	@Override
	public void run() {
		if(!run)
			return;
		while(run){
			try {
				byte[] packetBuf = new byte[15*1024];
				DatagramPacket packet = new DatagramPacket(packetBuf, packetBuf.length);
				socket.setSoTimeout(30*1000);
				socket.receive(packet);
				InetAddress playerAddr = packet.getAddress();
				int playerPort = packet.getPort();
				if(playing)
					if(!playerAddr.equals(currentPlayerAddr) && playerPort!=currentPlayerPort)
						continue;
				parseMessage(packet);
			} catch (SocketTimeoutException e){
				/*
				 * Connection timeout
				 */
				if(playing){
					ConnectionTimeout toMsg = new ConnectionTimeout();
					try {
						socket.send(toMsg.getMsgPacket(currentPlayerAddr, currentPlayerPort));
						byte[] packetBuf = new byte[15*1024];
						DatagramPacket packet = new DatagramPacket(packetBuf, packetBuf.length);
						socket.setSoTimeout(5*1000);
						socket.receive(packet);
						if(!packet.getAddress().equals(currentPlayer)&&packet.getPort()!=currentPlayerPort)
							closeSession();
					} catch (IOException e2) {
						System.out.println("Player "+currentPlayer+" connection timeout");
						closeSession();
					}
				}
				
			}catch (IOException e) {
				/*
				 * Connection closed
				 */
				
			}
			
		}

		
	}
	
	public void closeSession(){
		playing = false;
		currentPlayer = null;
		currentPlayerAddr = null;
		currentPlayerPort = 0;
		keepalive.stopIt();
		sendEvent(new NewGame());
		sendEvent(new PlayerDisconnected(remotePlayerName));
	}
	
	public void stopServer(){
		run = false;
		if(keepalive!=null)
			keepalive.stopIt();
		try {
			if(playing)
				socket.send(new CloseConnection().getMsgPacket(currentPlayerAddr, currentPlayerPort));
		} catch (IOException e) {
		}
		socket.close();
	}
	
	private void parseMessage(DatagramPacket packet){
		String message = new String(packet.getData()).trim();
        DocumentBuilder db;
		try {
			db = dbf.newDocumentBuilder();
	        InputSource is = new InputSource();
	        is.setCharacterStream(new StringReader(message));
	        System.out.println(System.currentTimeMillis()+" " +message);
	        Document doc = db.parse(is);
	        doc.normalize();
	        NodeList msgElements = doc.getElementsByTagName("khetMessage");
	        for(int i=0;i<msgElements.getLength();i++){
	        	Element khetMessage;
	        	if(!(msgElements.item(i) instanceof Element))
	        		continue;
	        	khetMessage = (Element)msgElements.item(i);
	        	String msgType = khetMessage.getAttribute("type");
	        	String playerName= khetMessage.getAttribute("player");
	        	String session = khetMessage.getAttribute("session");
	        	String msgId = khetMessage.getAttribute("id");
	        	if(msgType.equals(GameNetMessages.keepalive)&&!playing){
        			CloseConnection closeMsg = new CloseConnection();
        			socket.send(closeMsg.getMsgPacket(packet.getAddress(), packet.getPort()));
	        	}
	        	if(msgType.equals(GameNetMessages.handshake) || !playing){
	        		/* TODO
	        		 * 
	        		 */
	        		try {
	        			Document msgDoc = db.newDocument();
	        			Element msg = msgDoc.createElement("khetMessage");
	        			msgDoc.appendChild(msg);
	        			msg.setAttribute("type", GameNetMessages.handshake);
	        			msg.setAttribute("color", serverColor.toString().toLowerCase());
	        			msg.setAttribute("player", serverName);
	        			String stateBoardMsg = board.gameState();
						msg.setAttribute("gameState", stateBoardMsg);
						/*
						 * TODO
						 * integrate with the user preference
						 */
						msg.setAttribute("color", serverColor.toString().toLowerCase());
						sendEvent(new ResetMatch());
						currentPlayerAddr = packet.getAddress();
						currentPlayerPort = packet.getPort();
						sendMsgToClient(msgDoc);
						currentPlayer = playerName;
						playing = true;
						keepalive = new KeepAliveDaemon(socket, currentPlayerAddr, currentPlayerPort);
						keepalive.start();
						sendEvent(new ConnectionEstablished(playerName, Team.GRAY, Team.RED));
						remotePlayerName = playerName;
					} catch (TransformerException e) {
						e.printStackTrace();
					}
					return;
					
	        	}
				if(msgType.equals(GameNetMessages.close)&&playing){
					/*
					 * It's opened to receive new msgs
					 */
					closeSession();
					return;
				}
					
				if(playerName==null||session==null||msgType==null||!playing)
					continue;
				if(packet.getAddress().equals(currentPlayerAddr)&&packet.getPort()==currentPlayerPort){
					/*
					 * Return to the client the state of the board on
					 * the server side
					 */
					if(msgType.equals(GameNetMessages.gimmiState)){
						try {
							Document gameState = board.getGameStateDocument();
							Document msgDoc = db.newDocument();
							Element msg = msgDoc.createElement("khetMessage");
							msgDoc.appendChild(msg);
							Node board = msgDoc.importNode(gameState.getFirstChild(), true);
							msg.appendChild(board);
							msg.setAttribute("type", GameNetMessages.serverState);
							sendMsgToClient(msgDoc);
							return;
						} catch (TransformerException e) {
							e.printStackTrace();
						}
					}
					
					
					if(msgType.equals(GameNetMessages.move)){
						String x1msg = khetMessage.getAttribute("x1");
						String y1msg = khetMessage.getAttribute("y1");
						String x2msg = khetMessage.getAttribute("x2");
						String y2msg = khetMessage.getAttribute("y2");
						if(x1msg==null||y1msg==null||x2msg==null||y2msg==null)
							continue;
						try{
							int x1 = Integer.parseInt(x1msg);
							int y1 = Integer.parseInt(y1msg);
							int x2 = Integer.parseInt(x2msg);
							int y2 = Integer.parseInt(y2msg);
							sendEvent(new MovePiece(x1, y1, x2, y2, playerColor));
							sendACK(GameNetMessages.move, msgId);
						}catch(NumberFormatException e){}
						return;
					}
					if(msgType.equals(GameNetMessages.rotate)){
						String xmsg = khetMessage.getAttribute("x");
						String ymsg = khetMessage.getAttribute("y");
						String direction = khetMessage.getAttribute("direction");
						if(xmsg==null||ymsg==null||direction==null)
							continue;
						try{
							int x = Integer.parseInt(xmsg);
							int y = Integer.parseInt(ymsg);
							sendEvent(new RotatePiece(x, y, direction,playerColor));
							sendACK(GameNetMessages.rotate, msgId);
						}catch(NumberFormatException e){}
						return;
					}
					if(msgType.equals(GameNetMessages.umountObelisk)){
						String x1msg = khetMessage.getAttribute("x1");
						String y1msg = khetMessage.getAttribute("y1");
						String x2msg = khetMessage.getAttribute("x2");
						String y2msg = khetMessage.getAttribute("y2");
						if(x1msg==null||y1msg==null||x2msg==null||y2msg==null)
							continue;
						try{
							int x1 = Integer.parseInt(x1msg);
							int y1 = Integer.parseInt(y1msg);
							int x2 = Integer.parseInt(x2msg);
							int y2 = Integer.parseInt(y2msg);
							sendEvent(new UmountObelisk(x1, y1, x2, y2, playerColor));
							sendACK(GameNetMessages.umountObelisk, msgId);
						}catch(NumberFormatException e){}
						return;
					}if(msgType.equals(GameNetMessages.ack)){
		        		String action = khetMessage.getAttribute("value");
		        		String state = khetMessage.getAttribute("gameState");
		        		if(state.equals(board.gameState()))
		        			return;
		        		System.out.println("Remote player is not synchronized after action ["+action+"]");
		        		closeSession();
		        	}

					if(msgType.equals(GameNetMessages.endGame)){
						closeSession();
						return;
					}
					if(msgType.equals(GameNetMessages.connectionTimeout)){
		        		KeepAlive ka = new KeepAlive();
		        		socket.send(ka.getMsgPacket(currentPlayerAddr, currentPlayerPort));
		        		if(!keepalive.isAlive()){
		        			keepalive.start();
		        		}
		        		return;
					}
				}
	        }
	        
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}
	
	private void sendACK(String value, String transaction){
		Document msgDoc;
		try {
			msgDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
			msgDoc.normalize();
			Element msg = msgDoc.createElement(GameNetMessages.khetMsg);
			msgDoc.appendChild(msg);
			msg.setAttribute("type", GameNetMessages.ack);
			msg.setAttribute("value", value);
			msg.setAttribute("transaction", transaction);
			String boardState = board.gameState();
			msg.setAttribute("gameState", boardState);
			sendMsgToClient(msgDoc);
		} catch (ParserConfigurationException e1) {
			e1.printStackTrace();
		} catch (TransformerFactoryConfigurationError e) {
			e.printStackTrace();
		} catch (TransformerException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public void sendMsgToClient(Document msg) throws TransformerFactoryConfigurationError, TransformerException, IOException{
		Transformer transformer = TransformerFactory.newInstance().newTransformer();
		transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
		transformer.setOutputProperty(OutputKeys.INDENT, "no");

		StreamResult result = new StreamResult(new StringWriter());
		Element khetMsg = (Element)msg.getFirstChild();
		Long msgId = new Random().nextLong();
		khetMsg.setAttribute("id", msgId+"");
		DOMSource source = new DOMSource(msg);
		transformer.transform(source, result);
		
		byte[] buf = result.getWriter().toString().getBytes();
		DatagramPacket packet = new DatagramPacket(buf, buf.length, currentPlayerAddr, currentPlayerPort);
		socket.send(packet);

	}
	
	public String nextSessionId(){
	    return new BigInteger(130, new SecureRandom()).toString(32);
	}

	@Override
	public void registerGameListener(GameEventListener listener) {
		if(listeners==null)
			listeners = new ArrayList<GameEventListener>();
		listeners.add(listener);
		
	}

	@Override
	public void sendEvent(GameMessage event) {
		if(listeners==null)
			return;
		for(GameEventListener listener:listeners){
			listener.gameEvent(event);
		}
		
	}
	
	public boolean sendEventBool(GameMessage event){
		if(listeners==null)
			return false;
		for(GameEventListener listener:listeners){
			if(!listener.gameEvent(event)){
				return false;
			}
		}
		return true;
	}

}
